//
// Created by Administrator on 2016/9/2.
//

#include "ShareHeaders.h"

using namespace std;
using namespace ZainGL;
using namespace ZainTL;

glm::vec3 ZainGL::WORLD_UP(0, 1, 0);

inline GLenum getGLTexInternalFormat(int channels);

Texture *ZainGL::loadTexture(const char *path, std::unordered_map<std::string, FileDatas> *bufferMap) {
    int width, height, channels;
    unsigned char *image = NULL;
    if (bufferMap == nullptr) {
        image = SOIL_load_image(path, &width, &height, &channels, SOIL_LOAD_AUTO);
    } else {
        auto mapIter = bufferMap->find(path);
        if (mapIter != bufferMap->end()) {
            FileDatas fileData = mapIter->second;
            image = SOIL_load_image_from_memory((unsigned char *) fileData.data, fileData.dataLen, &width, &height,
                                                &channels, SOIL_LOAD_AUTO);
        } else {
            image = SOIL_load_image(path, &width, &height, &channels, SOIL_LOAD_AUTO);
        }
    }
    if (image != NULL) {
        GLuint textureID = 0;
        glGenTextures(1, &textureID);
        glBindTexture(GL_TEXTURE_2D, textureID);

        // Set texture wrapping
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        // Set texture filtering
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        GLenum format = getGLTexInternalFormat(channels);
        glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, image);
        glGenerateMipmap(GL_TEXTURE_2D);

        SOIL_free_image_data(image);
        glBindTexture(GL_TEXTURE_2D, 0); // Unbind texture when done, so we won't accidentily mess up our texture.

        Texture *texture = new Texture();
        texture->textureID = textureID;
        texture->path = path;
        return texture;
    }
    return nullptr;
}

Texture *
::ZainGL::loadCubeMap(const char *right, const char *left, const char *top, const char *bottom, const char *back,
                      const char *front, unordered_map<string, FileDatas> *bufferMap) {
    vector<const char *> paths = {right, left, top, bottom, back, front};

    GLuint textureID = 0;
    glGenTextures(1, &textureID);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);

    for (GLuint i = 0; i < 6; ++i) {
        const char *path=paths[i];
        int width, height, channels;
        unsigned char *image = NULL;
        if (bufferMap == nullptr) {
            image = SOIL_load_image(path, &width, &height, &channels, SOIL_LOAD_AUTO);
        } else {
            auto mapIter = bufferMap->find(path);
            if (mapIter != bufferMap->end()) {
                FileDatas fileData = mapIter->second;
                image = SOIL_load_image_from_memory((unsigned char *) fileData.data, fileData.dataLen, &width, &height,
                                                    &channels, SOIL_LOAD_AUTO);
            } else {
                image = SOIL_load_image(path, &width, &height, &channels, SOIL_LOAD_AUTO);
            }
        }
        if (image != NULL) {
            GLenum format = getGLTexInternalFormat(channels);
            glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, image);
            SOIL_free_image_data(image);
        }
    }
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);

    glBindTexture(GL_TEXTURE_CUBE_MAP, 0); // Unbind texture when done, so we won't accidentily mess up our texture.

    Texture *texture = new Texture();
    texture->textureID = textureID;
    return texture;
}

GLenum getGLTexInternalFormat(int channels) {
    GLenum texFormat;
    switch (channels) {
        case 1:
            texFormat = GL_LUMINANCE;
            break;
        case 2:
            texFormat = GL_LUMINANCE_ALPHA;
            break;
        case 3:
            texFormat = GL_RGB;
            break;
        case 4:
            texFormat = GL_RGBA;
            break;
    }
    return texFormat;
}

const GLchar *ZainGL::getFileText(const GLchar *path) {
    File *file = new File(path);
    FileInputStream *fileInputStream = file->getFileInputStream();

    uint64_t size = fileInputStream->available();
    GLchar *result = new GLchar[size];
    fileInputStream->read(result, size);
    return result;
}

void
::ZainGL::screenPosToWorldRay(int32_t mouseX, int32_t mouseY, uint32_t screenWidth, uint32_t screenHeight,
                              glm::mat4 viewMatrix,
                              glm::mat4 projectionMatrix, glm::vec3 &outOrigin, glm::vec3 &outDirection) {
    // The ray Start and End positions, in Normalized Device Coordinates (Have you read Tutorial 4 ?)
    float mouseXNDC = ((float) mouseX / screenWidth - 0.5f) * 2.0f;//[0,screenWidth] -> [-1,1]
    float mouseYNDC = ((float) (screenHeight - mouseY) / screenHeight - 0.5f) * 2.0f;//[0,screenHeight] -> [-1,1]
    glm::vec4 lRayStart_NDC(mouseXNDC, mouseYNDC, -1.0,
                            1.0f);//The near plane maps to Z=-1 in Normalized Device Coordinates
    glm::vec4 lRayEnd_NDC(mouseXNDC, mouseYNDC, 0.0, 1.0f);


    // The Projection matrix goes from Camera Space to NDC.
    // So inverse(ProjectionMatrix) goes from NDC to Camera Space.
    /*glm::mat4 ProjectionMatrixInverse = glm::inverse(ProjectionMatrix);
    glm::vec4 lRayStart_camera = ProjectionMatrixInverse * lRayStart_NDC;
    lRayStart_camera /= lRayStart_camera.w;
    glm::vec4 lRayEnd_camera = ProjectionMatrixInverse * lRayEnd_NDC;
    lRayEnd_camera /= lRayEnd_camera.w;

    // The View Matrix goes from World Space to Camera Space.
    // So inverse(ViewMatrix) goes from Camera Space to World Space.
    glm::mat4 ViewMatrixInverse = glm::inverse(ViewMatrix);
    glm::vec4 lRayStart_world = ViewMatrixInverse * lRayStart_camera;
    lRayStart_world /= lRayStart_world.w;
    glm::vec4 lRayEnd_world = ViewMatrixInverse * lRayEnd_camera;
    lRayEnd_world /= lRayEnd_world.w;*/


    // Faster way (just one inverse)
    glm::mat4 M = glm::inverse(projectionMatrix * viewMatrix);
    glm::vec4 lRayStart_world = M * lRayStart_NDC;
    lRayStart_world /= lRayStart_world.w;
    glm::vec4 lRayEnd_world = M * lRayEnd_NDC;
    lRayEnd_world /= lRayEnd_world.w;

    outOrigin = glm::vec3(lRayStart_world);

    outDirection = glm::normalize(glm::vec3(lRayEnd_world - lRayStart_world));
}
